﻿#include "camera.h"
#include "image.h"
#include "material.h"
#include "pdf.h"


Camera::Camera(const std::string&file):outFile(file){}

void Camera::render(const Hittable& world, const Hittable& lights)
{
        initialize();

        PPM outfile(outFile,image_width,image_height);

        outfile.stream() << "P3\n" << image_width << ' ' << image_height << "\n255\n";

        for (int j = 0; j < image_height; j++) 
        { 
            for (int i = 0; i < image_width; i++) 
            {
                Color pixel_color (0,0,0);

                for(int s_j = 0;s_j < sqrt_sapp;s_j++)
                {
	                for(int s_i = 0; s_i < sqrt_sapp;s_i++)
	                {
                        Ray r = get_ray(i, j,s_i,s_j);
                        pixel_color += ray_color(r, max_depth, world, lights);
	                }
                }
                write_color(outfile.stream(), pixel_samples_scale  * pixel_color);
            }
        }
    }

void Camera::initialize()
{
        image_height = int(image_width / aspect_ratio);
        image_height = (image_height < 1) ? 1 : image_height;

        pixel_samples_scale = 1.0 / samples_per_pixel;

        sqrt_sapp = int(sqrt(samples_per_pixel));
        pixel_samples_scale = 1.0 / (sqrt_sapp * sqrt_sapp);
        recip_sqrt_spp = 1.0 / sqrt_sapp;

        center = lookfrom;

        // Determine viewport dimensions.
        //auto focal_length = (lookfrom - lookat).length();
        auto theta = degrees_to_radians(vfov);
		auto h = tan(theta / 2);
        auto viewport_height = 2.0 * h * focus_dist;
        auto viewport_width = viewport_height * (double(image_width)/image_height);

        w = unit_vector(lookfrom - lookat);
        u = unit_vector(cross(vup, w));
        v = cross(w, u);

        // Calculate the vectors across the horizontal and down the vertical viewport edges.
        auto viewport_u = viewport_width * u;
        auto viewport_v = viewport_height * -v;

        // Calculate the horizontal and vertical delta vectors from pixel to pixel.
        pixel_delta_u = viewport_u / image_width;
        pixel_delta_v = viewport_v / image_height;

        // Calculate the location of the upper left pixel.
        auto viewport_upper_left = center - (focus_dist * w) - viewport_u / 2 - viewport_v / 2;
        pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);

        // Calculate the camera defocus disk basis vectors.
        auto defocus_radius = focus_dist * tan(degrees_to_radians(defocus_angle / 2));
        defocus_disk_u = u * defocus_radius;
        defocus_disk_v = v * defocus_radius;
}


bool scattering_pdf_of_light(const HitRecord&rec,double&pdf, Ray& scattered,double time)
{
    /*
        The problem with sampling uniformly over all directions 
        is that lights are no more likely to be sampled than any arbirary or unimportant direction.
        We could use shadow rays to solve for the direct lighting at any given point. 
        Instead, I’ll just use a PDF that sends more rays to the light.
        We can then turn around and change that PDF to send more rays in whatever direction we want.

        It’s really easy to pick a random direction toward the light; just pick a random point on the light
        and send a ray in that direction. But we'll need to know the PDF, p(ω)
        , so that we're not biasing our render. But what is that?
    */
    auto on_light = Point3(random_double(213, 343), 554, random_double(227, 332));

    auto to_light = on_light - rec.p;
    auto distance_squared = to_light.length_squared();
    to_light = unit_vector(to_light);

    if (dot(to_light, rec.normal) < 0)
        return false;

    double light_area = (343 - 213) * (332 - 227);
    auto light_cosine = fabs(to_light.y());
    if (light_cosine < 0.000001)
        return false;

    pdf = distance_squared / (light_cosine * light_area);
    scattered = Ray(rec.p, to_light, time);

    return true;
}

/*
  Let’s assume we can send a bunch of rays toward the light source using a PDF pLight(ωo)
. Let’s also assume we have a PDF related to pScatter, and let’s call that pSurface(ωo)
. A great thing about PDFs is that you can just use linear mixtures of them to form mixture densities that are also PDFs. For example, the simplest would be:

  p(ωo)=1/2 pSurface(ωo)+ 1/2 pLight(ωo)

  As long as the weights are positive and add up to one, any such mixture of PDFs is a PDF. 
  Remember, we can use any PDF: all PDFs eventually converge to the correct answer. So, the game is to figure out how to make the PDF larger where the product
  pScatter(x,ωi,ωo)⋅Colori(x,ωi) is largest. 
  
  For diffuse surfaces, this is mainly a matter of guessing where Colori(x,ωi) is largest. Which is equivalent to guessing where the most light is coming from.
  For a mirror, pScatter() is huge only near one direction, so pScatter() matters a lot more. In fact, most renderers just make mirrors a special case, and make the pScatter()/p() implicit — our code currently does that.

*/

Color Camera::ray_color(const Ray& r, int depth,const Hittable& world, const Hittable& lights) const
{
    // If we've exceeded the ray bounce limit, no more light is gathered.
    if (depth <= 0)
        return Color(0, 0, 0);

    HitRecord rec;
    Ray scattered;
    Color attenuation;
    double pdf;

    // If the ray hits nothing, return the background color.
    if (!world.hit(r, Interval(0.001, infinity), rec))
        return background;

    Color color_from_emission = rec.material->emitted(r, rec, rec.u, rec.v, rec.p);

    if (false)
    {

        if (!rec.material->scatter(r, rec, attenuation, scattered, pdf))
            return  color_from_emission;

        if (true)
        {
            HittablePdf light_pdf(lights, rec.p);
            scattered = Ray(rec.p, light_pdf.generate(), r.time());
            pdf = light_pdf.value(scattered.direction());
        }
        else
        {
            auto p0 = make_shared<HittablePdf>(lights, rec.p);
            auto p1 = make_shared<CosinePdf>(rec.normal);
            MixturePdf mixed_pdf(p0, p1);

            scattered = Ray(rec.p, mixed_pdf.generate(), r.time());
            pdf = mixed_pdf.value(scattered.direction());
        }
    }
    else
    {
        ScatterRecord srec;

        if (!rec.material->scatter(r, rec, srec))
            return color_from_emission;

        if (srec.skip_pdf) {
            return srec.attenuation * ray_color(srec.skip_pdf_ray, depth - 1, world, lights);
        }

        auto light_ptr = make_shared<HittablePdf>(lights, rec.p);
        MixturePdf p(light_ptr, srec.pdf_ptr);

        scattered = Ray(rec.p, p.generate(), r.time());
        pdf = p.value(scattered.direction());

        attenuation = srec.attenuation;
    }

    double scattering_pdf = rec.material->scattering_pdf(r, rec, scattered);

    Color sample_color = ray_color(scattered, depth - 1, world, lights);

    Color color_from_scatter =
        (attenuation * scattering_pdf * sample_color) / pdf;

    return color_from_emission + color_from_scatter;
}

Ray Camera::get_ray(int i, int j , int s_i, int s_j) const
{
    auto offset  = sample_square_stratified(s_i, s_j);
    auto pixel_sample = pixel00_loc
        + ((i + offset.x()) * pixel_delta_u)
        + ((j + offset.y()) * pixel_delta_v);

    //auto ray_origin = center;
    auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample();
    auto ray_direction = pixel_sample - ray_origin;

    auto ray_time = random_double();

    return Ray(ray_origin, ray_direction, ray_time);
}

Vec3 Camera::sample_square() const
{
    // Returns the vector to a random point in the [-.5,-.5]-[+.5,+.5] unit square.
    return Vec3(random_double() - 0.5, random_double() - 0.5, 0);
}

Point3 Camera::defocus_disk_sample() const
{
    auto p = random_in_unit_disk();
    return center + (p[0] * defocus_disk_u) + (p[1] * defocus_disk_v);
}

Vec3 Camera::sample_square_stratified(int s_i, int s_j) const
{
    // Returns the vector to a random point in the square sub-pixel specified by grid
        // indices s_i and s_j, for an idealized unit square pixel [-.5,-.5] to [+.5,+.5].

    auto px = ((s_i + random_double()) * recip_sqrt_spp) - 0.5;
    auto py = ((s_j + random_double()) * recip_sqrt_spp) - 0.5;

    return Vec3(px, py, 0);
}

/*

So far I have the ray_color() function create two hard-coded PDFs:

p0() related to the shape of the light
p1() related to the normal vector and type of surface
We can pass information about the light (or whatever hittable we want to sample) into the ray_color() function, and we can ask the material function for a PDF (we would have to add instrumentation to do that). We also need to know if the scattered ray is specular, and we can do this either by asking the hit() function or the material class.

Diffuse Versus Specular
One thing we would like to allow for is a material — like varnished wood — that is partially ideal specular (the polish) and partially diffuse (the wood). Some renderers have the material generate two rays: one specular and one diffuse. I am not fond of branching, so I would rather have the material randomly decide whether it is diffuse or specular. The catch with that approach is that we need to be careful when we ask for the PDF value, and ray_color() needs to be aware of whether this ray is diffuse or specular. Fortunately, we have decided that we should only call the pdf_value() if it is diffuse, so we can handle that implicitly.

We can redesign material and stuff all the new arguments into a class like we did for hittable:
 
*/
 